// Copyright (C) 2010 Jesse Jones
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;

internal struct Field
{
	public Field(string name, string value, int line) : this()
	{
		Debug.Assert(!string.IsNullOrEmpty(name), "name is empty or null");
		Debug.Assert(value != null, "value is null");
		Debug.Assert(line >= 1, "line isn't positive");
		
		Name = name;
		Value = value;
		Line = line;
	}
	
	public string Name {get; private set;}
	public string Value {get; private set;}
	public int Line {get; private set;}

	public override string ToString()
	{
		if (Value.Length < 32)
			return string.Format("{0} = '{1}'", Name, Value);
		else
			return string.Format("{0} = '{1}...'", Name, Value.Substring(0, 32));
	}
}

// Simple field oriented parser based on a personal project. The syntax is as follows:
//
// Comment lines start with a '#' character.
//
// Blank lines contain only whitespace and are ignored (if they are outside of a field).
//
// Text fields start with an identifier immediately followed by a ':'. The text appears after the 
// colon and, if multiple lines are used, the additional lines must be indented. Whitespace at 
// the start and end of the text are stripped off. Runs of interior whitespace are replaced by 
// a single space.
//
// Literal fields start with an identifier immediately followed by a '!'. The text appears after 
// the colon and, if multiple lines are used, the additional lines must be indented. Whitespace 
// is left as is.
//
// Identifiers start with a letter followed by alpha-numeric characters, underscores, and dashes.
//
// Field names need not be unique.
internal static class FieldParser
{
	// Used to optionally filter each line in a test or literal field.
	public delegate string Filter(string fieldName, string line);

	public static Field[] Parse(string contents)
	{
		return Parse(contents, null);
	}

	public static Field[] Parse(string[] lines)
	{
		return Parse(lines, null);
	}

	public static Field[] Parse(string contents, Filter filter)
	{
		var lines = new List<string>();
		
		int i = 0;
		while (i < contents.Length)
		{
			int j = contents.IndexOfAny(new[]{'\r', '\n'}, i);
			if (j >= 0)
			{
				if (j + 1 < contents.Length && contents[j] == '\r' && contents[j + 1] == '\n')
					++j;
				lines.Add(contents.Substring(i, j - i + 1));
				i = j + 1;
			}
			else
			{
				lines.Add(contents.Substring(i));
				i = contents.Length;
			}
		}
		
		return Parse(lines.ToArray(), filter);
	}

	public static Field[] Parse(string[] lines, Filter filter)
	{
		var fields = new List<Item>();
		
		bool canContinue = false;
		for (int i = 0; i < lines.Length; ++i)
		{
			string line = lines[i];
			
			// field
			if (line.Length > 1 && char.IsLetter(line[0]))
			{
				fields.Add(DoParseField(line, i + 1, filter));
				canContinue = true;
			}
			
			// continued
			else if (line.Length > 0 && line[0] == '\t')
			{
				if (!canContinue)
					throw new FormatException("Line " + (i + 1) + " is a continued line, but the previous line is not a field or continued line");

				if (filter != null)
					line = filter(fields[fields.Count - 1].Name, line);

				fields[fields.Count - 1].Value.Append(line);
				canContinue = true;
			}
			
			// comment
			else if (line.Length >= 2 && line.StartsWith("#"))
			{
				canContinue = false;
				continue;
			}
			
			// blank
			else if (line == "\n" || line == "\r" || line == "\r\n" || line.Length == 0)
			{
				canContinue = false;
				continue;
			}
			
			// error
			else
				throw new FormatException("Couldn't parse line " + (i + 1));
		}
		
		Field[] result = new Field[fields.Count];
		for (int j = 0; j < fields.Count; ++j)
			result[j] = fields[j].ToField();
		
		return result;
	}
	
	#region Private Methods
	private static Item DoParseField(string line, int lineNum, Filter filter)
	{
		int i = line.IndexOfAny(new[]{':', '!'});
		if (i <= 0)
			throw new FormatException("Line " + lineNum + " has no colon or exclamation mark");
		
		string name = line.Substring(0, i);
		string value = line.Substring(i+1);
		
		if (filter != null)
			value = filter(name, value);
		
		return new Item(name, value, lineNum, line[i] == ':');
	}
	
	private static string DoSqueeze(string str, char[] from, char to)
	{
		StringBuilder builder = new StringBuilder(str.Length);
		
		int i = 0;
		while (i < str.Length)
		{
			if (Array.IndexOf(from, str[i]) >= 0)
			{
				builder.Append(to);
				while (i < str.Length && Array.IndexOf(from, str[i]) >= 0)
					++i;
			}
			else
				builder.Append(str[i++]);
		}
		
		return builder.ToString();
	}
	#endregion
	
	#region Private Types
	private struct Item
	{
		public Item(string name, string value, int line, bool textField) : this()
		{
			Debug.Assert(!string.IsNullOrEmpty(name), "name is empty or null");
			Debug.Assert(value != null, "value is null");
			Debug.Assert(line >= 1, "value isn't positive");
			
			Name = name;
			Value = new StringBuilder();
			Line = line;
			TextField = textField;
			
			Value.Append(value);
		}
		
		public Field ToField()
		{
			string value = Value.ToString();
			if (TextField)
				value = DoSqueeze(value.Trim(), new[]{' ', '\r', '\n', '\t'}, ' ');
			
			return new Field(Name, value, Line);
		}
		
		public string Name {get; private set;}
		public StringBuilder Value {get; private set;}
		public int Line {get; private set;}
		public bool TextField {get; private set;}
	}
	#endregion
}
